package com.gitee.wsl.doc.prosemirror.model.dom.parser

import com.fleeksoft.ksoup.nodes.Element
import com.fleeksoft.ksoup.nodes.TextNode
import com.fleeksoft.ksoup.nodes.Node as DomNode
import com.gitee.wsl.doc.prosemirror.model.attr.Attrs
import com.gitee.wsl.doc.prosemirror.model.mark.Mark
import com.gitee.wsl.doc.prosemirror.model.dom.ContentElement
import com.gitee.wsl.doc.prosemirror.model.dom.OPT_OPEN_LEFT
import com.gitee.wsl.doc.prosemirror.model.dom.OPT_PRESERVE_WS
import com.gitee.wsl.doc.prosemirror.model.dom.OPT_PRESERVE_WS_FULL
import com.gitee.wsl.doc.prosemirror.model.dom.parser.rule.ParseOptionsRule
import com.gitee.wsl.doc.prosemirror.model.dom.PreserveWhitespace
import com.gitee.wsl.doc.prosemirror.model.dom.parser.rule.StyleParseRule
import com.gitee.wsl.doc.prosemirror.model.dom.parser.rule.TagParseRule
import com.gitee.wsl.doc.prosemirror.model.dom.blockTags
import com.gitee.wsl.doc.prosemirror.model.dom.ignoreTags
import com.gitee.wsl.doc.prosemirror.model.dom.listTags
import com.gitee.wsl.doc.prosemirror.model.dom.markMayApply
import com.gitee.wsl.doc.prosemirror.model.dom.normalizeList
import com.gitee.wsl.doc.prosemirror.model.dom.styles
import com.gitee.wsl.doc.prosemirror.model.dom.wsOptionsFor
import com.gitee.wsl.doc.prosemirror.model.evaluateXpathNode
import com.gitee.wsl.doc.prosemirror.model.node.Node
import com.gitee.wsl.doc.prosemirror.model.node.NodeType
import com.gitee.wsl.doc.prosemirror.model.util.contains
import kotlin.collections.get
import kotlin.math.max

class ParseContext(
    // The parser we are using.
    val parser: DOMParser,
    // The options passed to this parse.
    val options: ParseOptions,
    val isOpen: Boolean
) {
    var open = 0
    val find: List<ParseOptionPosition>?
    var needsBlock: Boolean
    var nodes: MutableList<NodeContext>
    var localPreserveWS = false

    val top: NodeContext
        get() = this.nodes[this.open]

    init {
        val topNode = options.topNode
        val topContext: NodeContext
        val topOptions = wsOptionsFor(null, options.preserveWhitespace, 0) or
            (if (isOpen) OPT_OPEN_LEFT else 0)
        if (topNode != null) {
            topContext = NodeContext(
                topNode.type, topNode.attrs, Mark.Companion.none, true,
                options.topMatch ?: topNode.type.contentMatch, topOptions
            )
        } else if (isOpen) {
            topContext = NodeContext(null, null, Mark.Companion.none, true, null, topOptions)
        } else {
            topContext = NodeContext(parser.schema.topNodeType, null, Mark.Companion.none, true, null, topOptions)
        }
        this.nodes = mutableListOf(topContext)
        this.find = options.findPositions
        this.needsBlock = false
    }

    // Add a DOM node to the content. Text is inserted as text node,
    // otherwise, the node is passed to `addElement` or, if it has a
    // `style` attribute, `addElementWithStyles`.
    fun addDOM(dom: DomNode, marks: List<Mark>) {
        if (dom is TextNode) {
            this.addTextNode(dom, marks)
        } else if (dom is Element) {
            this.addElement(dom, marks)
        }
    }

    @Suppress("NestedBlockDepth", "ComplexCondition")
    fun addTextNode(dom: TextNode, marks: List<Mark>) {
        var value = dom.getWholeText()
        val top = this.top
        val preserveWSFull = (top.options and OPT_PRESERVE_WS_FULL) != 0
        val preserveWS = this.localPreserveWS || (top.options and OPT_PRESERVE_WS) > 0
        if (
            preserveWSFull ||
            top.inlineContext(dom) ||
            // /[^ \t\r\n\u000c]/.test(value)
            "[^ \\t\\r\\n\\u000c]".toRegex().containsMatchIn(value)
        ) {
            if (!preserveWS) {
                // value = value.replace(/[ \t\r\n\u000c]+/g, " ")
                value = value.replace("[ \\t\\r\\n\\u000c]+".toRegex(), " ")
                // If this starts with whitespace, and there is no node before it, or
                // a hard break, or a text node that ends with whitespace, strip the
                // leading space.
                // /^[ \t\r\n\u000c]/.test(value)
                if ("^[ \\t\\r\\n\\u000c]".toRegex().containsMatchIn(value) && this.open == this.nodes.size - 1) {
                    val nodeBefore = top.content.lastOrNull()
                    val domNodeBefore = dom.previousSibling()
                    // /[ \t\r\n\u000c]$/.test(nodeBefore.text!)
                    if (nodeBefore == null || domNodeBefore?.nodeName().equals("br", true) ||
                        (nodeBefore.isText && "[ \\t\\r\\n\\u000c]$".toRegex().containsMatchIn(nodeBefore.text!!))
                    ) {
                        value = value.substring(1)
                    }
                }
            } else if (!preserveWSFull) {
                // value = value.replace(/\r?\n|\r/g, " ")
                value = value.replace("\\r?\\n|\\r".toRegex(), " ")
            } else {
                // value = value.replace(/\r\n?/g, "\n")
                value = value.replace("\\r\\n?".toRegex(), "\n")
            }
            if (value.isNotEmpty()) {
                this.insertNode(this.parser.schema.text(value), marks)
            }
            this.findInText(dom)
        } else {
            this.findInside(dom)
        }
    }

    // Try to find a handler for the given tag and use that to parse. If
    // none is found, the element's content nodes are added directly.
    @Suppress("ComplexMethod")
    fun addElement(dom: Element, marks: List<Mark>, matchAfter: TagParseRule? = null) {
        val outerWS = this.localPreserveWS
        var top = this.top
        val hasStyleWhitespace = dom.styles()?.get("white-space")?.let { "pre".toRegex().containsMatchIn(it) } == true
        if (dom.tagName().uppercase() == "PRE" || hasStyleWhitespace) {
            this.localPreserveWS = true
        }
        var current = dom
        val name = current.nodeName().lowercase()
        var ruleID: TagParseRule? = null
        if (listTags.contains(name) && this.parser.normalizeLists) normalizeList(current)
        val ruleFromNode = this.options.ruleFromNode?.invoke(current)
        val rule = ruleFromNode ?: this.parser.matchTag(current, this, matchAfter).also { ruleID = it }
        if (rule?.ignore ?: ignoreTags.contains(name)) {
            this.findInside(current)
            this.ignoreFallback(current, marks)
        } else if (rule == null || rule.hasSkip() || rule.closeParent == true) {
            val ruleSkip = (rule as? ParseOptionsRule)?.skip
            if (rule?.closeParent == true) {
                this.open = max(0, this.open - 1)
            } else if (ruleSkip is Element) {
                current = ruleSkip
            }
            var sync = false
            val oldNeedsBlock = this.needsBlock
            if (blockTags.contains(name)) {
                if (top.content.isNotEmpty() && top.content[0].isInline && this.open != 0) {
                    this.open--
                    top = this.top
                }
                sync = true
                if (top.type == null) {
                    this.needsBlock = true
                }
            } else if (current.firstChild() == null) {
                this.leafFallback(current, marks)
                this.localPreserveWS = outerWS
                return
            }
            val innerMarks = if (rule != null && rule.hasSkip()) {
                marks
            } else {
                this.readStyles(current, marks)
            }
            if (innerMarks != null) {
                this.addAll(current, innerMarks)
            }
            if (sync) {
                this.sync(top)
            }
            this.needsBlock = oldNeedsBlock
        } else {
            val innerMarks = this.readStyles(current, marks)
            if (innerMarks != null) {
                this.addElementByRule(
                    current,
                    rule as TagParseRule,
                    innerMarks,
                    ruleID?.takeIf { rule.consuming == false }
                )
            }
        }
        this.localPreserveWS = outerWS
    }

    // Called for leaf DOM nodes that would otherwise be ignored
    fun leafFallback(dom: DomNode, marks: List<Mark>) {
        if (dom.nodeName().equals("br", true) && this.top.type?.inlineContent == true) {
            this.addTextNode(TextNode("\n"), marks)
        }
    }

    // Called for ignored nodes
    fun ignoreFallback(dom: DomNode, marks: List<Mark>) {
        // Ignored BR nodes should at least create an inline context
        if (dom.nodeName().equals("br", true) && (this.top.type?.inlineContent == false)) {
            this.findPlace(this.parser.schema.text("-"), marks)
        }
    }

    // Run any style parser associated with the node's styles. Either
    // return an updated array of marks, or null to indicate some of the
    // styles had a rule with `ignore` set.
    @Suppress("LoopWithTooManyJumpStatements", "NestedBlockDepth")
    fun readStyles(dom: Element, marks: List<Mark>): List<Mark>? {
        var result = marks.toMutableList()
        val styles = dom.styles()
        // Because many properties will only show up in 'normalized' form
        // in `style.item` (i.e. text-decoration becomes
        // text-decoration-line, text-decoration-color, etc), we directly
        // query the styles mentioned in our rules instead of iterating
        // over the items.
        if (!styles.isNullOrEmpty()) {
            for (i in this.parser.matchedStyles.indices) {
                val name = this.parser.matchedStyles[i]
                val value = styles[name]
                if (value != null) {
                    var after: StyleParseRule? = null
                    while (true) {
                        val rule = this.parser.matchStyle(name, value, this, after) ?: break
                        if (rule.ignore == true) {
                            return null
                        }
                        if (rule.clearMark != null) {
                            result = result.filter { m -> !rule.clearMark!!(m) }.toMutableList()
                        } else {
                            result += this.parser.schema.marks[rule.mark]!!.create(rule.attrs)
                        }
                        if (rule.consuming == false) {
                            after = rule
                        } else {
                            break
                        }
                    }
                }
            }
        }
        return result
    }

    // Look up a handler for the given node. If none are found, return
    // false. Otherwise, apply it, use its return value to drive the way
    // the node's content is wrapped, and return true.
    @Suppress("ComplexMethod")
    fun addElementByRule(dom: Element, rule: TagParseRule, marks: List<Mark>, continueAfter: TagParseRule?) {
        var sync = false
        var nodeType: NodeType? = null
        val ruleNode = rule.node
        var updatedMarks = marks
        if (ruleNode != null) {
            nodeType = this.parser.schema.nodeType(ruleNode)
            if (!nodeType.isLeaf) {
                val inner = this.enter(nodeType, rule.attrs, updatedMarks, rule.preserveWhitespace)
                if (inner != null) {
                    sync = true
                    updatedMarks = inner
                }
            } else if (!this.insertNode(nodeType.create(rule.attrs), updatedMarks)) {
                this.leafFallback(dom, updatedMarks)
            }
        } else {
            val markType = this.parser.schema.marks[rule.mark!!]!!
            updatedMarks = updatedMarks + markType.create(rule.attrs)
        }
        val startIn = this.top

        if (nodeType?.isLeaf == true) {
            this.findInside(dom)
        } else if (continueAfter != null) {
            this.addElement(dom, updatedMarks, continueAfter)
        } else if (rule.getContent != null) {
            this.findInside(dom)
            rule.getContent!!.invoke(dom, this.parser.schema)?.forEach { node, _, _ ->
                this.insertNode(node, updatedMarks)
            }
        } else {
            var contentDOM = dom
            val contentElement = rule.contentElement
            if (contentElement is ContentElement.StringContentElement) {
                contentDOM = evaluateXpathNode(contentElement.s, dom)
            } else if (contentElement is ContentElement.FunctionContentElement) {
                contentDOM = contentElement.func(dom)
            } else if (contentElement is ContentElement.ElementContentElement) {
                contentDOM = contentElement.element
            }
            this.findAround(dom, contentDOM, true)
            this.addAll(contentDOM, updatedMarks)
            this.findAround(dom, contentDOM, false)
        }
        if (sync && this.sync(startIn)) this.open--
    }

    // Add all child nodes between `startIndex` and `endIndex` (or the
    // whole node, if not given). If `sync` is passed, use it to
    // synchronize after every block element.
    fun addAll(parent: DomNode, marks: List<Mark>, startIndex: Int? = null, endIndex: Int? = null) {
        var index = startIndex ?: 0
        var dom = if (startIndex != null) parent.childNode(startIndex) else parent.firstChild()
        val end = endIndex?.let { parent.childNode(endIndex) }
        while (dom != end) {
            this.findAtPoint(parent, index)
            this.addDOM(dom!!, marks)
            dom = dom.nextSibling()
            ++index
        }
        this.findAtPoint(parent, index)
    }

    // Try to find a way to fit the given node type into the current
    // context. May add intermediate wrappers and/or leave non-solid
    // nodes that we're in.
    @Suppress("LoopWithTooManyJumpStatements")
    fun findPlace(node: Node, marks: List<Mark>): List<Mark>? {
        var route: List<NodeType>? = null
        var sync: NodeContext? = null
        var updateMarks = marks
        for (depth in this.open downTo 0) {
            val cx = this.nodes[depth]
            val found = cx.findWrapping(node)
            if (found != null && (route == null || route.size > found.size)) {
                route = found
                sync = cx
                if (found.isEmpty()) break
            }
            if (cx.solid) break
        }
        if (route == null) return null
        this.sync(sync!!)
        route.forEach {
            updateMarks = this.enterInner(it, null, updateMarks, false)
        }
        return updateMarks
    }

    // Try to insert the given node, adjusting the context when needed.
    fun insertNode(node: Node, marks: List<Mark>): Boolean {
        var updatedMarks = marks
        if (node.isInline && this.needsBlock && this.top.type == null) {
            val block = this.textblockFromContext()
            if (block != null) updatedMarks = this.enterInner(block, null, marks)
        }
        val innerMarks = this.findPlace(node, updatedMarks)
        if (innerMarks != null) {
            this.closeExtra()
            val top = this.top
            if (top.match != null) top.match = top.match!!.matchType(node.type)
            var nodeMarks = Mark.Companion.none
            for (m in innerMarks + node.marks) {
                val add = if (top.type != null) {
                    top.type.allowsMarkType(m.type)
                } else {
                    markMayApply(m.type, node.type)
                }
                if (add) nodeMarks = m.addToSet(nodeMarks)
            }
            top.content.add(node.mark(nodeMarks))
            return true
        }
        return false
    }

    // Try to start a node of the given type, adjusting the context when
    // necessary.
    fun enter(type: NodeType, attrs: Attrs?, marks: List<Mark>, preserveWS: PreserveWhitespace?): List<Mark>? {
        var innerMarks = this.findPlace(type.create(attrs), marks)
        if (innerMarks != null) innerMarks = this.enterInner(type, attrs, marks, true, preserveWS)
        return innerMarks
    }

    // Open a node of the given type
    fun enterInner(
        type: NodeType,
        attrs: Attrs? = null,
        marks: List<Mark>,
        solid: Boolean = false,
        preserveWS: PreserveWhitespace? = null
    ): List<Mark> {
        this.closeExtra()
        val top = this.top
        top.match = top.match?.matchType(type)
        var options = wsOptionsFor(type, preserveWS, top.options)
        if ((top.options and OPT_OPEN_LEFT) != 0 && top.content.isEmpty()) {
            options = options or OPT_OPEN_LEFT
        }
        var applyMarks = Mark.Companion.none
        val result = marks.filter { m ->
            val ok = if (top.type != null) {
                top.type.allowsMarkType(m.type)
            } else {
                markMayApply(m.type, type)
            }
            if (ok) {
                applyMarks = m.addToSet(applyMarks)
                false
            } else {
                true
            }
        }
        this.nodes.add(NodeContext(type, attrs, applyMarks, solid, null, options))
        this.open++
        return result
    }

    // Make sure all nodes above this.open are finished and added to
    // their parents
    fun closeExtra(openEnd: Boolean = false) {
        var i = this.nodes.size - 1
        if (i > this.open) {
            while (i > this.open) {
                this.nodes[i - 1].content.add(this.nodes[i].finish(openEnd) as com.gitee.wsl.doc.prosemirror.model.node.Node)
                i--
            }
            this.nodes = this.nodes.take(this.open + 1).toMutableList()
        }
    }

    fun finish(): Any {
        this.open = 0
        this.closeExtra(this.isOpen)
        return this.nodes[0].finish(this.isOpen || this.options.topOpen == true)
    }

    fun sync(to: NodeContext): Boolean {
        for (i in this.open downTo 0) {
            if (this.nodes[i] == to) {
                this.open = i
                return true
            } else if (this.localPreserveWS) {
                this.nodes[i].options = this.nodes[i].options or OPT_PRESERVE_WS
            }
        }
        return false
    }

    val currentPos: Int
        get() {
            this.closeExtra()
            var pos = 0
            for (i in this.open downTo 0) {
                val content = this.nodes[i].content
                for (j in content.size - 1 downTo 0)
                    pos += content[j].nodeSize
                if (i != 0) pos++
            }
            return pos
        }

    fun findAtPoint(parent: DomNode, offset: Int) {
        this.find?.forEach {
            if (it.node == parent && it.offset == offset) {
                it.pos = this.currentPos
            }
        }
    }

    fun findInside(parent: DomNode) {
        this.find?.forEach {
            if (it.pos == null && (parent is Element) && parent.contains(it.node)) {
                it.pos = this.currentPos
            }
        }
    }

    @Suppress("MagicNumber", "NestedBlockDepth", "UnusedPrivateMember")
    fun findAround(parent: DomNode, content: DomNode, before: Boolean) {
//        if (parent != content) {
//            this.find?.forEach {
//                if (it.pos == null && (parent is Element) && parent.children().contains(it.node)) {
//                    val pos = content.compareDocumentPosition(it.node).toInt()
//                    if ((pos and (if (before) 2 else 4)) != 0) {
//                        it.pos = this.currentPos
//                    }
//                }
//            }
//        }
    }

    fun findInText(textNode: TextNode) {
        this.find?.forEach {
            if (it.node == textNode) {
                it.pos = this.currentPos - (textNode.getWholeText().length - it.offset)
            }
        }
    }

    // Determines whether the given context string matches this context.
    @Suppress("ComplexMethod", "NestedBlockDepth")
    fun matchesContext(context: String): Boolean {
        if (context.indexOf("|") > -1) {
            val regex = "\\s*\\|\\s*".toRegex()
            return context.split(regex).firstOrNull {
                matchesContext(it)
            } != null
        }

        val parts = context.split("/")
        val option = this.options.context
        val useRoot = !this.isOpen && (option == null || option.parent.type == this.nodes[0].type)
        val minDepth = -(if (option != null) option.depth + 1 else 0) + (if (useRoot) 0 else 1)

        @Suppress("ReturnCount")
        fun match(i: Int, depth: Int): Boolean {
            var depth = depth
            for (i in i downTo 0) {
                val part = parts[i]
                if (part == "") {
                    if (i == parts.size - 1 || i == 0) {
                        continue
                    }
                    while (depth >= minDepth) {
                        if (match(i - 1, depth)) {
                            return true
                        }
                        depth--
                    }
                    return false
                } else {
                    val next = if (depth > 0 || (depth == 0 && useRoot)) {
                        this.nodes[depth].type
                    } else if (option != null && depth >= minDepth) {
                        option.node(depth - minDepth).type
                    } else {
                        null
                    }
                    if (next == null || (next.name != part && !next.isInGroup(part))) {
                        return false
                    }
                    depth--
                }
            }
            return true
        }

        return match(parts.size - 1, this.open)
    }

    @Suppress("ReturnCount")
    fun textblockFromContext(): NodeType? {
        val context = this.options.context
        if (context != null) {
            for (d in context.depth downTo 0) {
                val deflt = context.node(d).contentMatchAt(context.indexAfter(d)).defaultType
                if (deflt != null && deflt.isTextblock) return deflt
            }
        }
        for (name in this.parser.schema.nodes) {
            val type = this.parser.schema.nodeType(name.key)
            if (type.isTextblock) return type
        }
        return null
    }
}